# Copyright 2009 Lee Harr
#
# This file is part of Acromania.
#
# Acromania is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Acromania is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Acromania.  If not, see <http://www.gnu.org/licenses/>.


from time import time
import random
from random import normalvariate
from bisect import bisect
import operator

from twisted.internet import reactor
later = reactor.callLater

import amdb
import conf
from colors import blue, red, green, yellow, magenta, white, bold

letterweights = conf.letterweights
totalweights = float(sum(letterweights[x] for x in letterweights))
letters = letterweights.keys()
letters.sort()
weights = [letterweights[letter] for letter in letters]
cum_norm_weights = [0.0]*len(weights)
for i in xrange(len(weights)):
    cum_norm_weights[i] = cum_norm_weights[i-1] + weights[i]/totalweights


def closest5(n):
    'return the value n rounded to the nearest 5'

    n = int(n)

    d, m = divmod(n, 5)
    if m > 2:
        d += 1
    return d * 5


class CMix(object):
    def command(self, line, player):
        try:
            cmd, rest = line.split(' ', 1)
        except ValueError:
            cmd = line
            rest = ''

        if cmd == 'new':
            self.gameserver.new_game(player)
        elif cmd == 'nick':
            self.change_name(player, rest)
        elif cmd == 'create':
            self.gameserver.create(player, rest)
        elif cmd == 'login':
            self.gameserver.login(player, rest)
        elif cmd == 'passwd':
            self.gameserver.passwd(player, rest)
        elif cmd == 'players':
            self.gameserver.show_players(player)
        elif cmd == 'score':
            self.gameserver.show_players(player)
        elif cmd == 'show':
            self.gameserver.game.show_status(player)
        elif cmd == 'acro':
            self.gameserver.game.register_acro(player, rest)
        elif cmd == 'vote':
            self.gameserver.game.register_vote(player, rest)
        elif cmd == 'help':
            self.help(player)
        elif cmd == 'rules':
            self.rules(player)
        elif cmd == 'top10':
            self.gameserver.top10(player, rest)
        elif cmd == 'leaders':
            self.gameserver.leaderboard(player)
        elif cmd == 'quit':
            self.leave()
        elif cmd == 'bot':
            self.gameserver.add_bot(player, rest)
        elif cmd == 'info':
            self.gameserver.contact(player)
        else:
            self.message('No such command: /%s' % cmd)


class DummyClient(object):
    def broadcast(self, msg='', indent=0, color=True, exclude=None):
        #print msg
        pass
    def message(self, *args, **kw):
        #msg = kw.get('msg', args[0])
        #print msg
        pass

class Game(object):
    def __init__(self, gameserver):
        self.started = False
        self.finished = False
        self.destroyed = False
        self.acro = ''
        time0 = time()
        self.time0 = time0
        self.gameserver = gameserver
        self.phase = 'starting'
        self.players = []
        self.scores = {}
        self.acros = {}
        self.changetime = 0

    def join(self, player):
        broadcast = self.gameserver.broadcast

        self.players.append(player)

        broadcast('%s joins the game.' % white(player.name), exclude=player)

        if len(self.players) >= conf.min_players and not self.started:
            t = time() + conf.between_rounds_time
            self.change_phase(t, 'starting')
            self.started = True

    def leave(self, player):
        self.players.remove(player)
        broadcast = self.gameserver.broadcast
        broadcast('%s has left the game' % white(player.name))
        if len(self.players) < conf.min_players:
            broadcast('Not enough players to play now...')
            if self.started:
                self.finished = True

    def show_players(self, player):
        player.message('Connected players:')
        plist = []
        namelen = 0
        scorelen = 0
        for p in self.players:
            score = self.scores.get(p, 0)
            plist.append((score, p))
            namelen = max(namelen, len(white(p.name)))
            scorelen = max(scorelen, len(str(yellow(score))))
        plist.sort(reverse=True)

        for score, p in plist:
            if not p.username:
                player.message('%*s: %s' % (scorelen, yellow(score), white(p.name)))
            else:
                player.message('%*s: %-*s [%s]' % (scorelen, yellow(score), namelen, white(p.name), p.username))

    def playernames(self):
        return [player.name for player in self.players]

    def player(self, playername):
        for player in self.players:
            if player.name == playername:
                return player

    def start(self):
        self.scores = {}
        self.round = 0
        broadcast = self.gameserver.broadcast
        broadcast('New game starting now!')
        self.newround()

    def change_phase(self, t, phase):
        # If a new game was started while this one was still in progress....
        if self.finished:
            return

        broadcast = self.gameserver.broadcast
        now = time()
        remaining = closest5(t - now)
        self.changetime = t
        info = {'acroing':
                    {'msg': 'New round starting in%s %s seconds.',
                        'func': self.newround},
                'voting':
                    {'msg': 'Acros due in%s %s seconds.',
                        'func': self.voting},
                'tallying':
                    {'msg': 'Votes due in%s %s seconds.',
                        'func': self.tally_votes},
                'starting':
                    {'msg': 'New game starting in%s %s seconds.',
                        'func': self.start},
                'finished':
                    {'msg': 'Game over. Type /new to start again.',
                        'func': self.game_over},
                }
        if remaining > 0:
            rawmsg = info[phase]['msg']
            if '%s' in rawmsg:
                space = ' ' if remaining < 10 else ''
                msg = rawmsg % (space, red(remaining))
            else:
                msg = rawmsg
            broadcast(msg)
        else:
            func = info[phase]['func']
            func()
            return

        if remaining <= 5:
            later(5, self.change_phase, t, phase)
        elif remaining <= 10:
            later(5, self.change_phase, t, phase)
        elif remaining <= 30:
            later(20, self.change_phase, t, phase)
        else:
            later(30, self.change_phase, t, phase)

    def acrolen(self):
        r = int(normalvariate(conf.acro_length_average, conf.acro_length_stddev))
        n = min(r, 7)
        n = max(n, 3)
        return n

    def test_acrolen(self):
        n = 10000
        lens = {}
        for i in range(n):
            alen = self.acrolen()
            if alen in lens:
                lens[alen] += 1
            else:
                lens[alen] = 1

        for alen in lens:
            print '%s %0.4f' % (alen, lens[alen]/float(n))

    def acroletter(self):
        return letters[bisect(cum_norm_weights, random.random())]

    def test_acroletter(self):
        n = 10000
        testletters = {}
        for i in range(n):
            letter = self.acroletter()
            if letter in testletters:
                testletters[letter] += 1
            else:
                testletters[letter] = 1

        for letter in letters:
            print '%s %0.4f %0.4f' % (letter,
                        letterweights[letter]/float(totalweights),
                        testletters[letter]/float(n))


    def newacro(self):
        n = self.acrolen()
        acro = []
        for i in range(n):
            l = self.acroletter()
            acro.append(l)
        self.acro = ''.join(acro)
        return self.acro

    def acro_legal(self, acro):
        acro = acro.upper()
        words = acro.split(' ')

        if len(words) != len(self.acro):
            return False

        for word, acroletter in zip(words, self.acro):
            for letter in word:
                if letter not in letterweights:
                    continue
                elif letter == acroletter:
                    break
                elif letter != acroletter:
                    return False

        return True

    def newround(self):
        broadcast = self.gameserver.broadcast
        self.round += 1
        self.newacro()
        self.acros = {}
        self.times = {}
        self.votes = {}
        self.phase = 'acroing'
        broadcast('The acro is: %s' % yellow(self.acro))
        broadcast('Register your acro with: /acro <Your Acro Here>')
        endtime = time() + conf.acroing_phase_time
        self.change_phase(endtime, 'voting')

    def register_acro(self, player, acro):
        if self.phase == 'voting':
            player.message(magenta('Too late to acro.'))
            return
        elif self.phase != 'acroing':
            player.message('No acro currently.')
            return

        a0 = ''
        while a0 != acro:
            a0 = acro
            acro = acro.replace('  ', ' ')

        if self.acro_legal(acro):
            if player not in self.acros:
                player.message('Your acro "%s" has been registered.' % blue(acro))
                changed = False
            else:
                player.message('Your acro is now "%s"' % blue(acro))
                changed = True
            self.acros[player] = acro
            self.times[player] = time() - self.time0
            self.register_acro_notice(player, changed)
            return True
        else:
            # Acro does not match the given letters
            player.message(magenta('Your acro is not valid!'))
            player.message('The acro is: %s' % yellow(self.acro))
            return False

    def register_acro_notice(self, player, changed):
        broadcast = self.gameserver.broadcast
        name = player.name
        if not changed and len(self.acros) == 1:
            broadcast('%s is first to answer and is eligible for speed points.' % white(name), exclude=player)
            player.message('You answered first and are eligible for speed points.')
        elif changed:
            broadcast('%s changes answers.' % white(name), exclude=player)
        else:
            broadcast('%s answers.' % white(name), exclude=player)

    def voting(self):
        broadcast = self.gameserver.broadcast

        for player in self.players:
            if player.ai:
                acro = player.make_acro(self.acro)
                if acro:
                    self.register_acro(player, acro)
                else:
                    broadcast('%s: I cannot think of an acro!' % white(player.name))

        self.phase = 'voting'
        if not self.acros:
            broadcast('No acros submitted.')
            self.phase = 'tallying'
            endtime = time() + conf.voting_phase_time_min
            self.change_phase(endtime, 'acroing')
            return

        players = [player for player in self.acros]
        random.shuffle(players)
        self.acro_map = players
        broadcast('Acros are due. Please begin voting.')
        broadcast('Choose your favorite acro:')
        for n, player in enumerate(self.acro_map):
            acro = self.acros[player]
            broadcast('%s: %s' % (white(n), blue(acro)))
        broadcast('Register your vote with: /vote <# of favorite acro>')
        n_acros = len(self.acros)
        tmin = conf.voting_phase_time_min
        tnacros = n_acros * conf.voting_phase_time_per_acro
        tnacrowords = len(self.acro)*n_acros*conf.voting_phase_time_per_acroword
        voting_time = max(tmin, tnacros, tnacrowords)
        endtime = time() + voting_time
        self.change_phase(endtime, 'tallying')

    def register_vote(self, player, vote):
        broadcast = self.gameserver.broadcast

        if self.phase == 'tallying':
            player.message(magenta('Too late to vote...'))
            return
        elif self.phase == 'acroing':
            player.message(magenta("It's not time to vote yet..."))
            player.message(magenta('Maybe you meant /acro ?'))
            return
        elif self.phase != 'voting':
            player.message("It's not time to vote.")
            return

        try:
            vote = int(vote)
        except ValueError:
            # did not send an integer vote
            return False

        nacros = len(self.acros)
        if 0 <= vote < nacros:
            if self.acro_map[vote] == player:
                player.message('You cannot vote for your own acro.')
                return False
            else:
                player.message('Your vote for acro #%s has been registered.' % vote)
                broadcast('%s votes.' % white(player.name), exclude=player)
                self.votes[player] = vote
                return True
        else:
            # voted for an acro that does not exist
            player.message('Must vote between 0 and %s' % nacros)
            return False

    def change_score(self, player, points):
        if player in self.scores:
            self.scores[player] += points
        else:
            self.scores[player] = points

    def tally_votes(self):
        for player in self.players:
            if player.ai:
                if player in self.acro_map:
                    vote = self.acro_map.index(player)
                else:
                    vote = 0
                breakout_count = 100
                while self.acro_map[vote] == player:
                    vote = player.make_vote(self.acros)
                    breakout_count -= 1
                    if breakout_count <= 0: break
                self.register_vote(player, vote)

        #print 'votes', self.votes
        self.phase = 'tallying'

        namelen = max(len(white(name)) for name in self.playernames())
        #print 'max name length', namelen

        broadcast = self.gameserver.broadcast
        broadcast('Voting is over!')

        # Note: switching to trickle broadcast for voting results!
        broadcast = self.gameserver.trickle

        votes = self.votes.values()
        if not votes and len(self.acro_map) != 1:
            broadcast = self.gameserver.broadcast
            broadcast('No votes cast.')
            endtime = time() + conf.between_rounds_time
            self.change_phase(endtime, 'acroing')
            return

        tally = {}
        find_most_votes = []
        for n in range(len(self.acro_map)):
            count = votes.count(n)
            tally[n] = count
            find_most_votes.append((count, n))
        #print 'tally', tally

        find_most_votes.sort()
        find_most_votes.reverse()
        most_votes_score = find_most_votes[0][0]
        most_votes = [x[1] for x in find_most_votes if x[0]==most_votes_score]
        n_votes = len(votes)

        broadcast('Voting results:')
        for n, player in enumerate(self.acro_map):
            acro = self.acros[player]
            t = tally.get(n, 0)
            broadcast('%s: %*s: %s: %s' % (n,
                                            namelen,
                                            white(player.name),
                                            yellow(t),
                                            blue(acro)))
            if n_votes:
                percent = int(float(t)*100/n_votes)
            else:
                percent = 0

            if not player.ai:
                amdb.save(self.acro, t, percent, acro, player.name)

        totals = tally.items()
        totals.sort(key=operator.itemgetter(1), reverse=True)
        times = self.times.items()
        times.sort(key=operator.itemgetter(1))
        fastest_player = times[0][0]

        # May need to give points to a player who did not acro
        for player in self.players:
            if player not in self.acro_map and player in self.votes:
                totals.append((player, -1))

        prev = None
        winner = None
        most_bonus = conf.points_for_most_votes
        for n, t in totals:
            only_acro = False
            if prev is not None and t != prev:
                most_bonus = 0

            if t >= 0 and n >= len(self.acro_map):
                # Player disconnected after getting a vote
                continue
            elif t < 0:
                # n may be the number or the actual player if the player
                #   did not acro, but voted
                player = n
            else:
                player = self.acro_map[n]

                # Don't penalize player if only 1 acro was submitted
                if len(self.acro_map) == 1:
                    only_acro = True

            prev = t
            if t == 1:
                rawmsg = '%s received %s vote.'
            else:
                rawmsg = '%s received %s votes.'

            if t >= 0:
                broadcast(rawmsg % (white(player.name), t))
                score = t * conf.points_per_vote
            else:
                # non-acroing voter
                score = 0

            if player in self.votes:
                if most_bonus and t:
                    winner = t
                    broadcast('%s wins a %s point bonus for receiving the most votes.' % (white(player.name), conf.points_for_most_votes))
                    score += conf.points_for_most_votes
                if player == fastest_player:
                    if t:
                        broadcast('%s wins a %s point bonus for speed.' % (white(player.name), conf.points_for_speed))
                        score += conf.points_for_speed
                    else:
                        broadcast('%s was fastest, but did not get any votes. No bonus!' % player.name)
                if player in self.votes and self.votes[player] in most_votes:
                    broadcast('%s wins a %s point bonus for voting for the most popular acro' % (white(player.name), conf.points_for_voting_for_most_popular))
                    score += conf.points_for_voting_for_most_popular
            elif only_acro:
                only_acro_score = conf.points_for_only_acro
                n_opponents = len(self.players)-1
                only_acro_score += n_opponents * conf.points_for_only_acro_per_opponent
                broadcast('%s wins %s points for submitting the only acro.' % (white(player.name), only_acro_score))
                score = only_acro_score
            else:
                if most_bonus and t:
                    score += conf.points_for_most_votes
                if player == fastest_player:
                    score += conf.points_for_speed
                add_s = '' if score==1 else 's'
                broadcast('%s would have scored %s point%s, but did not vote.' % (white(player.name), score, add_s))
                score = 0

            if score == 1:
                rawmsg = '%s scores %s point.'
            else:
                rawmsg = '%s scores %s points.'
            broadcast(rawmsg % (white(player.name), yellow(score)))

            self.change_score(player, score)

        scores = []
        for player in self.players:
            score = self.scores.get(player, 0)
            scores.append((score, player))
        scores.sort()
        scores.reverse()

        broadcast('Score update:')
        for s, player in scores:
            space = ' ' if s < 10 else ''
            msg = '%s%s: %s' % (space, yellow(s), white(player.name))
            broadcast(msg)

        score = scores[0][0]
        if score >= conf.points_to_win_game:
            winners = [sp for sp in scores if sp[0]==score]
            if len(winners) == 1:
                winner = winners[0][1]
                broadcast('%s wins!' % white(winner.name))
                broadcast()
                later(15, self.game_over)
                later(30, self.gameserver.between_games)
                for s, player in scores:
                    if not player.username: continue
                    won = player==winner
                    amdb.leader(player.username, won, s)
            else:
                broadcast('A tie! We need an EXTRA ROUND to find a winner!')
                endtime = time() + conf.between_rounds_time
                self.change_phase(endtime, 'acroing')

        else:
            endtime = time() + conf.between_rounds_time
            self.change_phase(endtime, 'acroing')

    def game_over(self):
        self.phase = 'finished'
        self.finished = True
        self.gameserver.game_over()

    def show_status(self, player):
        phase = self.phase
        t = self.changetime
        remaining = int(t - time())
        if phase == 'starting' and self.started:
            player.message('Game starting in %s seconds.' % red(remaining))

        elif phase == 'starting':
            player.message('Need more players.')

        elif phase == 'acroing':
            player.message('Round already in progress.')
            player.message('The acro is: %s' % yellow(self.acro))
            player.message('Acros due in %s seconds.' % red(remaining))

        elif phase == 'voting':
            player.message('Round in progress.')
            player.message('Voting ends in %s seconds.' % red(remaining))

        elif phase == 'tallying':
            player.message('New round starting in %s seconds.' % red(remaining))


if __name__ == '__main__':
    import sys
    if sys.argv[1] == 'len':
        g = Game(None)
        g.test_acrolen()
